<!DOCTYPE html>
<head xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
    <meta charset="utf-8">

    <script src="vendor/d3/d3-3.3.4.js"></script>
    <script src="vendor/jquery/jquery-1.11.1.min.js"></script>
    <script src="vendor/stickytableheaders/jquery.stickytableheaders.min.js"></script>
    <script src="vendor/bootstrap/js/bootstrap.js"></script>
    <script src="vendor/bootstrap-sortable/bootstrap-sortable.js"></script>

    <link href="vendor/bootstrap/css/bootstrap.css" rel="stylesheet" type="text/css">
    <link href="vendor/bootstrap-sortable/bootstrap-sortable.css" rel="stylesheet" type="text/css">

    <link rel="stylesheet" href="vendor/highlightjs/7.3/styles/idea.min.css">
    <script src="vendor/highlightjs/7.3/highlight.min.js"></script>

    <style type="text/css">
        pre code {
            white-space: pre;
            white-space: pre-wrap;
        }

        .node circle {
            cursor: pointer;
            fill: #fff;
            stroke: steelblue;
            stroke-width: 1.5px;
        }

        .node text {
            font-size: 12px;
        }

        path.link {
            fill: none;
            stroke: #ccc;
            stroke-width: 1.5px;
        }

        #tasks th {
            background-color: #fff;
            padding-left: 15px;
            padding-right: 10px;
        }

        #tasks td {
            padding-left: 15px;
        }

        #tasks td:nth-child(4),
        #tasks td:nth-child(5),
        #tasks td:nth-child(6),
        #tasks td:nth-child(7),
        #tasks td:nth-child(8),
        #tasks td:nth-child(9),
        #tasks td:nth-child(10),
        #tasks td:nth-child(11),
        #tasks td:nth-child(12) {
            text-align: right;
        }

        #failureMessage pre {
            font-size: 10px;
            line-height: 14px;
            white-space: pre;
            word-wrap: normal;
            overflow-y: hidden;
        }
    </style>
</head>

<body>


<div class="container">
    <div class="page-header">
        <h1>Presto :: Query <span id="queryId"></span></h1>
    </div>

    <h2>Summary</h2>

    <dl class="dl-horizontal">
        <dt>Query State</dt>
        <dd id="queryState"></dd>

        <dt>User</dt>
        <dd id="sessionUser"></dd>

        <dt>Principal</dt>
        <dd id="sessionPrincipal"></dd>

        <dt>Source</dt>
        <dd id="sessionSource"></dd>

        <dt>Catalog</dt>
        <dd id="sessionCatalog"></dd>

        <dt>Schema</dt>
        <dd id="sessionSchema"></dd>

        <dt>Create Time</dt>
        <dd id="createTime"></dd>

        <dt>Elapsed</dt>
        <dd id="elapsedTime"></dd>

        <dt>Queued</dt>
        <dd id="queuedTime"></dd>

        <dt>End Time</dt>
        <dd id="endTime"></dd>

        <dt>Memory Pool</dt>
        <dd id="memoryPool"></dd>

        <dt>Total Memory</dt>
        <dd id="totalMemory"></dd>

        <dt>CPU Time</dt>
        <dd id="cpuTime"></dd>

        <dt>Rows</dt>
        <dd id="rows"></dd>

        <dt>Data Size</dt>
        <dd id="dataSize"></dd>

        <dt>Cumulative Memory</dt>
        <dd id="cumulativeMemory"></dd>

        <dt>Raw</dt>
        <dd><a id="rawJson"></a></dd>

        <dt>Plan</dt>
        <dd><a id="executionLink"></a></dd>

        <dt>Visualization</dt>
        <dd><a id="timelineLink">Timeline</a></dd>

        <dt>Error Type</dt>
        <dd id="errorType"></dd>

        <dt>Error Code</dt>
        <dd id="errorCode"></dd>

        <dt>Message</dt>
        <dd id="failureMessage"></dd>

        <dt>Session Properties</dt>
        <dd id="sessionProperties"></dd>

        <dt>Kill</dt>
        <dd id="killQuery"></dd>
    </dl>

    <h2>Query</h2>

    <div class="sql"><pre><code id="query"></code></pre></div>

    <h2>Stages</h2>

    <div id="stages"></div>

    <h2>Tasks</h2>

    <table id="tasks" class="table table-striped sortable">
        <thead>
        <tr>
            <th data-defaultsort="asc">Id</th>
            <th>Host</th>
            <th>State</th>
            <th>Rows</th>
            <th>Rows/s</th>
            <th>Bytes</th>
            <th>Bytes/s</th>
            <th>Splits Pending</th>
            <th>Splits Running</th>
            <th>Splits Done</th>
            <th>Output Buffers</th>
            <th>Buffered Pages</th>
        </tr>
        </thead>
    </table>
</div>

</body>

<script>


d3.json('/v1/query/' + window.location.search.substring(1), function (query) {
    d3.select('#query').text(query.query);
    d3.select('#queryId').text(query.queryId);
    if (query.queryStats.fullyBlocked && query.state == "RUNNING") {
        var state = "BLOCKED";
        if (query.queryStats.blockedReasons.length > 0) {
            state += " (" + query.queryStats.blockedReasons.join() + ")";
        }
        d3.select('#queryState').text(state);
    }
    else {
        d3.select('#queryState').text(query.state);
    }
    d3.select('#sessionUser').text(query.session.user);
    d3.select('#sessionPrincipal').text(query.session.principal);
    d3.select('#sessionSource').text(query.session.source);
    d3.select('#sessionCatalog').text(query.session.catalog);
    d3.select('#sessionSchema').text(query.session.schema);
    d3.select('#createTime').text(formatDateTime(new Date(query.queryStats.createTime)));
    d3.select('#elapsedTime').text(query.queryStats.elapsedTime);
    d3.select('#queuedTime').text(query.queryStats.queuedTime);
    if (query.queryStats.endTime) {
        d3.select('#endTime').text(formatDateTime(new Date(query.queryStats.endTime)));
    }
    d3.select('#memoryPool').text(query.memoryPool);
    d3.select('#totalMemory').text(query.queryStats.totalMemoryReservation);
    d3.select('#cpuTime').text(query.queryStats.totalCpuTime);
    d3.select('#rows').text(formatCount(query.queryStats.rawInputPositions));
    d3.select('#dataSize').text(query.queryStats.rawInputDataSize);
    d3.select('#cumulativeMemory').text(formatCumulativeMemory(query.queryStats.cumulativeMemory));

    var button = d3.select('#killQuery')
        .append('button')
        .text('Kill')
        .attr('type', 'button');
    if (query.state != 'FINISHED' && query.state != 'FAILED' && query.state != 'CANCELED') {
        button.on('click', function() {
                d3.xhr("/v1/query/" + query.queryId).send('DELETE');
                button.attr('disabled', 'disabled');
            });
    } else {
        button.attr('disabled', 'disabled');
    }

    if (query.failureInfo) {
        var s = formatStackTrace(query.failureInfo);
        d3.select('#failureMessage').html("<pre>" + htmlEscape(s) + "</pre>");
    }
    else {
        d3.select('#failureMessage').html("&nbsp;");
    }

    if (query.errorType) {
        d3.select('#errorType').text(query.errorType);
    }
    else {
        d3.select('#errorType').html("&nbsp;");
    }

    if (query.errorCode) {
        d3.select('#errorCode').text(query.errorCode.name + " (" + query.errorCode.code + ")");
    }
    else {
        d3.select('#errorCode').html("&nbsp;");
    }

    var sessionPropertiesList = d3.select('#sessionProperties').append("ul");

    for (property in query.session.systemProperties) {
        if (query.session.systemProperties.hasOwnProperty(property)) {
            sessionPropertiesList.append("li").text(property + "=" + query.session.systemProperties[property]);
        }
    }

    for (catalog in query.session.catalogProperties) {
        if (query.session.catalogProperties.hasOwnProperty(catalog)) {
            for (property in query.session.catalogProperties[catalog]) {
                if (query.session.catalogProperties[catalog].hasOwnProperty(property)) {
                    sessionPropertiesList.append("li").text(catalog + "." + property + "=" + query.session.catalogProperties[catalog][property]);
                }
            }
        }
    }

    var uri = "/v1/query/" + query.queryId + "?pretty"
    d3.select('#rawJson').text(uri);
    d3.select('#rawJson').attr("href", uri);

    var executionLink = "/ui/plan?" + query.queryId;
    d3.select('#executionLink').text(executionLink);
    d3.select('#executionLink').attr("href", executionLink);

    var timelineLink = "/timeline.html?" + query.queryId;
    d3.select('#timelineLink').attr("href", timelineLink);

    hljs.initHighlighting();

    renderStagesTree(query.outputStage);

    renderTable('tasks', getTasks(query.outputStage));
    $.bootstrapSortable(applyLast=true);
    $('#tasks').stickyTableHeaders();
});

function formatDateTime(date) {
    return d3.time.format("%Y-%m-%d %H:%M:%S %Z")(date);
}

function formatCumulativeMemory(cumulativeMemory) {
    return (cumulativeMemory / Math.pow(1000.0, 4)).toLocaleString() + 'GB seconds';
}

function formatStackTrace(info) {
    return doFormatStackTrace(info, [], "", "");
}

function doFormatStackTrace(info, parentStack, prefix, linePrefix) {
    var s = linePrefix + prefix + failureInfoToString(info) + "\n";

    if (info.stack != null) {
        var sharedStackFrames = 0;
        if (parentStack != null) {
            sharedStackFrames = countSharedStackFrames(info.stack, parentStack);
        }

        for (var i = 0; i < info.stack.length - sharedStackFrames; i++) {
            s += linePrefix + "\tat " + info.stack[i] + "\n";
        }
        if (sharedStackFrames !== 0) {
            s += linePrefix + "\t... " + sharedStackFrames + " more" + "\n";
        }
    }

    if (info.suppressed != null) {
        for (var i = 0; i < info.suppressed.length; i++) {
            s += doFormatStackTrace(info.suppressed[i], info.stack, "Suppressed: ", linePrefix + "\t");
        }
    }

    if (info.cause != null) {
        s += doFormatStackTrace(info.cause, info.stack, "Caused by: ", linePrefix);
    }

    return s;
}

function countSharedStackFrames(stack, parentStack) {
    var n = 0;
    var minStackLength = Math.min(stack.length, parentStack.length);
    while (n < minStackLength && stack[stack.length - 1 - n] === parentStack[parentStack.length - 1 - n]) {
        n++;
    }
    return n;
}

function failureInfoToString(t) {
    return (t.message != null) ? (t.type + ": " + t.message) : t.type;
}

function htmlEscape(str) {
    return String(str)
            .replace(/&/g, '&amp;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
}

function removeQueryId(id)
{
    var pos = id.indexOf('.');
    if (pos != -1) {
        return id.substring(pos + 1);
    }

    return id
}

function taskIdToTinySortCompatibleString(id) {
    var taskIdArr = removeQueryId(id).split(".");
    return taskIdArr.map( function(numStr) { return new Array(5 - numStr.length).join("0") + numStr }).join("|");
}

function getTasks(stage) {
    return [].concat.apply(
            stage.tasks.map(taskData),
            stage.subStages.map(getTasks));
}

function taskData(d) {
    var elapsed = parseDuration(d.stats.elapsedTime);
    var rows = d.stats.rawInputPositions;
    var bytes = parseDataSize(d.stats.rawInputDataSize);

    var allBuffers = d.outputBuffers.buffers.length;
    var finishedBuffers = d3.sum(d.outputBuffers.buffers, function (i) {
        return i.finished;
    });
    var bufferedPages = d3.sum(d.outputBuffers.buffers, function (i) {
        return i.bufferedPages;
    });

    return [
        {"display": "<a href=" + d.self + "?pretty>" + htmlEscape(d.taskId) + "</a>", "sortableString": taskIdToTinySortCompatibleString(d.taskId)},
        {"display": htmlEscape(urlHostname(d.self))},
        {"display": htmlEscape(formatState(d.state, d.stats.fullyBlocked))},
        {"display": htmlEscape(formatCount(rows)), "sortableString": rows},
        {"display": htmlEscape(formatCount(computeRate(rows, elapsed))), "sortableString": computeRate(rows, elapsed)},
        {"display": htmlEscape(formatDataSize(bytes)), "sortableString": bytes},
        {"display": htmlEscape(formatDataSize(computeRate(bytes, elapsed))), "sortableString": computeRate(bytes, elapsed)},
        {"display": htmlEscape(d.stats.queuedDrivers)},
        {"display": htmlEscape(d.stats.runningDrivers)},
        {"display": htmlEscape(d.stats.completedDrivers)},
        {"display": htmlEscape(finishedBuffers + ' / ' + allBuffers)},
        {"display": htmlEscape(bufferedPages)}
    ];
}

function urlHostname(url) {
    var s = new URL(url).hostname;
    if ((s.charAt(0) == '[') && (s.charAt(s.length - 1) == ']')) {
        s = s.substr(1, s.length - 2);
    }
    return s;
}

function formatState(state, fullyBlocked) {
    if (fullyBlocked && state == "RUNNING") {
        return "BLOCKED";
    }
    else {
        return state;
    }
}


/** Computes rate per second **/
function computeRate(count, ms) {
    if (ms == 0) {
        return 0;
    }
    return (count / ms) * 1000.0;
}

function formatCount(count) {
    var unit = "";
    if (count > 1000) {
        count /= 1000;
        unit = "K";
    }
    if (count > 1000) {
        count /= 1000;
        unit = "M";
    }
    if (count > 1000) {
        count /= 1000;
        unit = "B";
    }
    if (count > 1000) {
        count /= 1000;
        unit = "T";
    }
    if (count > 1000) {
        count /= 1000;
        unit = "Q";
    }
    return precisionRound(count) + unit;
}


function parseDataSize(value) {
    var DATA_SIZE_PATTERN = /^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]+)\s*$/
    var match = DATA_SIZE_PATTERN.exec(value);
    if (match == null) {
        return null;
    }
    var number = parseFloat(match[1]);
    switch (match[2]) {
        case "B":
            return number;
        case "kB":
            return number * Math.pow(2, 10);
        case "MB":
            return number * Math.pow(2, 20);
        case "GB":
            return number * Math.pow(2, 30);
        case "TB":
            return number * Math.pow(2, 40);
        case "PB":
            return number * Math.pow(2, 50);
        default:
            return null;
    }
}

function formatDataSize(size) {
    var unit = "B";
    if (size >= 1024) {
        size /= 1024;
        unit = "K";
    }
    if (size >= 1024) {
        size /= 1024;
        unit = "M";
    }
    if (size >= 1024) {
        size /= 1024;
        unit = "G";
    }
    if (size >= 1024) {
        size /= 1024;
        unit = "T";
    }
    if (size >= 1024) {
        size /= 1024;
        unit = "P";
    }
    return precisionRound(size) + unit;
}

function parseDuration(value) {
    var DURATION_PATTERN = /^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]+)\s*$/

    var match = DURATION_PATTERN.exec(value);
    if (match == null) {
        return null;
    }
    var number = parseFloat(match[1]);
    switch (match[2]) {
        case "ns":
            return number / 1000000.0;
        case "us":
            return number / 1000.0;
        case "ms":
            return number;
        case "s":
            return number * 1000;
        case "m":
            return number * 1000 * 60;
        case "h":
            return number * 1000 * 60 * 60;
        case "d":
            return number * 1000 * 60 * 60 * 24;
        default:
            return null;
    }
}

function precisionRound(n) {
    if (n < 10) {
        return d3.round(n, 2);
    }
    if (n < 100) {
        return d3.round(n, 1);
    }
    return Math.round(n);
}

function renderTable(id, data) {
    var body = d3.select('#' + id).append("tbody");

    var rows = body.selectAll("tr")
            .data(data)
            .enter()
            .append("tr");

    var cells = rows.selectAll("td")
            .data(function (d) {
                return d;
            })
            .enter()
            .append("td")
            .html(function (d) {
                return d["display"]
            })
            .attr("data-value", function(d) {
                if ("sortableString" in d) {
                    return d["sortableString"];
                }
            });
}

function renderStagesTree(source) {
    var m = [20, 120, 20, 120];
    var w = 940 - (m[1] - m[3]);
    var h = 150 - (m[0] - m[2]);
    var i = 0;
    var root;

    var tree = d3.layout.tree()
            .size([h, w]);

    var diagonal = d3.svg.diagonal()
            .projection(function (d) {
                return [d.y, d.x];
            });

    var vis = d3.select("#stages").append("svg:svg")
            .attr("width", w + m[1] + m[3])
            .attr("height", h + m[0] + m[2])
            .append("svg:g")
            .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

    copyChildren(source);

    root = source;
    root.x0 = h / 2;
    root.y0 = 0;

    update(root);

    function copyChildren(d) {
        d.children = d.subStages;
        if (d.children) {
            d.children.forEach(copyChildren);
        }
    }

    function update(source) {
        var duration = d3.event && d3.event.altKey ? 5000 : 500;

        // Compute the new tree layout.
        var nodes = tree.nodes(root).reverse();

        // Normalize for fixed-depth.
        nodes.forEach(function (d) {
            d.y = d.depth * 200;
        });

        // Update the nodes…
        var node = vis.selectAll("g.node")
                .data(nodes, function (d) {
                    return d.id || (d.id = ++i);
                });

        // Enter any new nodes at the parent's previous position.
        var nodeEnter = node.enter().append("svg:g")
                .attr("class", "node")
                .attr("transform", function (d) {
                    return "translate(" + source.y0 + "," + source.x0 + ")";
                })
                .on("click", function (d) {
                    toggle(d);
                    update(d);
                });

        nodeEnter.append("svg:circle")
                .attr("r", 1e-6)
                .style("fill", function (d) {
                    return d._children ? "lightsteelblue" : "#fff";
                });

        nodeEnter.append("svg:text")
                .attr("x", function (d) {
                    return d.children || d._children ? -10 : 10;
                })
                .attr("dy", ".35em")
                .attr("text-anchor", function (d) {
                    return d.children || d._children ? "end" : "start";
                })
                .text(function (d) {
                    return removeQueryId(d.stageId) + ' ' + formatState(d.state, d.stageStats.fullyBlocked);
                })
                .style("fill-opacity", 1e-6);

        // Transition nodes to their new position.
        var nodeUpdate = node
//                .transition()
//                .duration(duration)
                .attr("transform", function (d) {
                    return "translate(" + d.y + "," + d.x + ")";
                });

        nodeUpdate.select("circle")
                .attr("r", 4.5)
                .style("fill", function (d) {
                    return d._children ? "lightsteelblue" : "#fff";
                });

        nodeUpdate.select("text")
                .style("fill-opacity", 1);

        // Transition exiting nodes to the parent's new position.
        var nodeExit = node.exit()
//                .transition()
//                .duration(duration)
                .attr("transform", function (d) {
                    return "translate(" + source.y + "," + source.x + ")";
                })
                .remove();

        nodeExit.select("circle")
                .attr("r", 1e-6);

        nodeExit.select("text")
                .style("fill-opacity", 1e-6);

        // Update the links…
        var link = vis.selectAll("path.link")
                .data(tree.links(nodes), function (d) {
                    return d.target.id;
                });

        // Enter any new links at the parent's previous position.
        link.enter().insert("svg:path", "g")
                .attr("class", "link")
                .attr("d", function (d) {
                    var o = {x: source.x0, y: source.y0};
                    return diagonal({source: o, target: o});
                })
//                .transition()
//                .duration(duration)
                .attr("d", diagonal);

        // Transition links to their new position.
        link
//                .transition()
//                .duration(duration)
                .attr("d", diagonal);

        // Transition exiting nodes to the parent's new position.
        link.exit()
//                .transition()
//                .duration(duration)
                .attr("d", function (d) {
                    var o = {x: source.x, y: source.y};
                    return diagonal({source: o, target: o});
                })
                .remove();

        // Stash the old positions for transition.
        nodes.forEach(function (d) {
            d.x0 = d.x;
            d.y0 = d.y;
        });
    }

    // Toggle children.
    function toggle(d) {
        if (d.children) {
            d._children = d.children;
            d.children = null;
        } else {
            d.children = d._children;
            d._children = null;
        }
    }
}
</script>
